package br.com.vti.leilao.model.business;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.SQLException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.Transient;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.apache.log4j.Logger;
import org.springframework.transaction.annotation.Transactional;

import br.com.vti.leilao.model.annotation.Constraint;
import br.com.vti.leilao.model.annotation.Constraints;
import br.com.vti.leilao.model.business.exception.ApplicationException;
import br.com.vti.leilao.model.entity.Model;
import br.com.vti.leilao.model.entity.ModelCombo;
import br.com.vti.leilao.model.util.ClassUtil;

/**
 * Controller utilizado para controllar as transações.
 *
 * @author Ednardo Rubens
 *
 * @param <E> entidade extends Model
 * @param <X> exceção extends Exception
 *
 */
@Transactional(readOnly = true)
public abstract class BusinessImpl<E extends Model<? extends Number>, X extends ApplicationException> implements
        Business<E, X> {

    private static final long serialVersionUID = -105364697960576417L;

    /**
     * Obtém getLogger() da classe
     * @return
     */
    protected Logger getLogger() {
        return Logger.getLogger(getClass().getName());
    }

    /*
     * (non-Javadoc) @see
     * br.com.vti.leilao.model.business.Business#inserir(br.com.vti.core.model.entity.Model)
     */
    @Override
    @Transactional(readOnly = false)
    public void inserir(E entidade) throws X {
        formatarEntidade(entidade);
        validate(INSERIR, entidade);
        try {
            getDao().inserir(entidade);
        } catch (Exception e) {
            lancarException(e, INSERIR);
        }
    }

    /*
     * (non-Javadoc) @see
     * br.com.vti.leilao.model.business.Business#atualizar(br.com.vti.core.model.entity.Model)
     */
    @Override
    @Transactional(readOnly = false)
    public void atualizar(E entidade) throws X {
        formatarEntidade(entidade);
        validate(ATUALIZAR, entidade);
        try {
            getDao().atualizar(entidade);
        } catch (Exception e) {
            lancarException(e, ATUALIZAR);
        }
    }

    /*
     * (non-Javadoc) @see
     * br.com.vti.leilao.model.business.Business#remover(br.com.vti.core.model.entity.Model)
     */
    @Override
    @Transactional(readOnly = false)
    public void remover(E entidade) throws X {
        validate(REMOVER, entidade);
        try {
            getDao().remover(entidade);
        } catch (Exception e) {
            lancarException(e, REMOVER);
        }
    }

    /*
     * (non-Javadoc) @see
     * br.com.vti.leilao.model.business.Business#procurarPorId(br.com.vti.core.model.entity.Model)
     */
    @Override
    public E procurarPorId(E entidade) throws X {
        validate(BUSCAR, entidade);
        E result = null;
        try {
            result = getDao().procurarPorId(entidade);
        } catch (Exception e) {
            lancarException(e, BUSCAR);
        }
        if (result == null) {
            throw getNewException(getMessageFactory().getMessage("item.noresult.buscar"));
        }
        return result;
    }

    /*
     * (non-Javadoc) @see
     * br.com.vti.leilao.model.business.Business#listarPorHql(java.lang.String,
     * java.util.Map, int)
     */
    @Override
    public List<E> listarPorHql(String hql, Map<String, Object> parametros, int maxResults) throws X {
        List<E> result = null;
        if (hql != null && !hql.isEmpty()) {
            try {
                result = getDao().listarPorHql(hql, parametros, maxResults);
            } catch (Exception e) {
                lancarException(e, LISTAR);
            }
            if (result == null || result.isEmpty()) {
                throw getNewException(getMessageFactory().getMessage("item.noresult.buscar"));
            }
        } else {
            throw getNewException(getErrorMsg(LISTAR) + " " + getMessageFactory().getMessage("item.erro.campos"));
        }
        return result;
    }

    /*
     * (non-Javadoc) @see
     * br.com.vti.leilao.model.business.Business#consultarPorHql(java.lang.String,
     * java.util.Map, java.lang.Integer)
     */
    @Override
    public List<Object[]> consultarPorHql(String hql, Map<String, Object> parametros, int maxResults) throws X {
        List<Object[]> result = null;
        if (hql != null && !hql.isEmpty()) {
            try {
                result = getDao().consultarPorHql(hql, parametros, maxResults);
            } catch (Exception e) {
                lancarException(e, LISTAR);
            }
            if (result == null || result.isEmpty()) {
                throw getNewException(getMessageFactory().getMessage("item.noresult.buscar"));
            }
        } else {
            throw getNewException(getErrorMsg(LISTAR) + " " + getMessageFactory().getMessage("item.erro.campos"));
        }
        return result;
    }

    /*
     * (non-Javadoc) @see
     * br.com.vti.leilao.model.business.Business#listarPorExemplo(br.com.vti.core.model.entity.Model,
     * int)
     */
    @Override
    public List<E> listarPorExemplo(E entidade, int maxResults) throws X {
        List<E> result = null;
        if (entidade != null) {
            toUpperCase(entidade);
            try {
                result = getDao().listarPorExemplo(entidade, maxResults);
            } catch (Exception e) {
                lancarException(e, LISTAR);
            }
        }
        if (result == null || result.isEmpty()) {
            throw getNewException(getMessageFactory().getMessage("item.noresult.buscar"));
        }
        return result;
    }

    /*
     * (non-Javadoc) @see
     * br.com.vti.leilao.model.business.Business#listarParaCombo(java.lang.Class)
     */
    @Override
    public List<E> listarParaCombo(Class<E> classe) throws X {
        List<E> result = null;
        if (classe != null && ModelCombo.class.isAssignableFrom(classe)) {
            try {
                result = getDao().listarParaCombo(classe);
            } catch (Exception e) {
                lancarException(e, LISTAR);
            }
        } else {
            throw getNewException(getMessageFactory().getMessage("item.erro.listarcombo"));
        }
        if (result == null || result.isEmpty()) {
            throw getNewException(getErrorMsg(LISTAR));
        }
        return result;
    }

    /*
     * (non-Javadoc) @see
     * br.com.vti.leilao.model.business.Business#validate(java.lang.Integer,
     * br.com.vti.core.model.entity.Model)
     */
    @Override
    public void validate(Integer operacao, E entidade) throws X {
        if (entidade == null) {
            throw getNewException(getMessageFactory().getMessage("item.erro.nulo"));
        } else {
            String erros = "";
            ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
            Validator validator = factory.getValidator();

            Set<ConstraintViolation<E>> constraintViolations = null;
            switch (operacao) {
                case ATUALIZAR:
                    constraintViolations = validator.validate(entidade);
                    break;
                case BUSCAR:
                case REMOVER:
                    constraintViolations = validator.validateProperty(entidade, "id");
                    break;
                case INSERIR:
                    constraintViolations = validator.validate(entidade);
                    ConstraintViolation<E> constraintViolationId = null;
                    for (ConstraintViolation<E> constraintViolation : constraintViolations) {
                        if (constraintViolation.getPropertyPath().toString().equalsIgnoreCase("id")) {
                            constraintViolationId = constraintViolation;
                        }
                    }
                    constraintViolations.remove(constraintViolationId);
                    break;
                default:
                    break;
            }
            for (ConstraintViolation<E> constraintViolation : constraintViolations) {
                String property = constraintViolation.getPropertyPath().toString();
                property = property.substring(0, 1).toUpperCase() + property.substring(1) + ": ";
                erros += " " + property + constraintViolation.getMessage();
            }
            if (!erros.isEmpty()) {
                throw getNewException(getErrorMsg(operacao));
            }
        }
    }

    /**
     * Método utilizado para gerar uma msg de erro
     *
     * @param operacao
     * @return mensagem de erro
     */
    protected String getErrorMsg(int operacao) {
        String msg = null;
        if (operacao == INSERIR) {
            msg = getErrorMsg("item.salvar");
        } else if (operacao == REMOVER) {
            msg = getErrorMsg("item.remover");
        } else if (operacao == ATUALIZAR) {
            msg = getErrorMsg("item.atualizar");
        } else if (operacao == BUSCAR) {
            msg = getErrorMsg("item.buscar");
        } else if (operacao == LISTAR) {
            msg = getErrorMsg("item.listar");
        }
        return msg;
    }

    /**
     * Método utilizado para gerar uma msg de erro
     *
     * @param key
     * @param parametros
     * @return mensagem de erro
     */
    protected String getErrorMsg(String key, Object... parametros) {
        return getMessageFactory().getMessage("item.erro.msg",
                getMessageFactory().getMessage(key, parametros));
    }

    /**
     * Método utilizado para gerar uma msg de erro
     *
     * @param operacao
     * @param key
     * @param parametros
     * @return mensagem de erro
     */
    protected String getErrorMsg(int operacao, String key, Object... parametros) {
        return getErrorMsg(operacao) + " " + getMessageFactory().getMessage(key, parametros);
    }

    /**
     * Retorna o tipo de entidade utilizada.
     *
     * @return classe classe da entidade
     */
    @SuppressWarnings("unchecked")
    protected Class<E> getEntityClass() {
        Class<E> theType = (Class<E>) ClassUtil.getTypeArguments(getClass(), 0);
        return theType;
    }

    /**
     * Retorna um construtor da entidade baseado no parametros passados.
     *
     * @param tipos parametros do construtor
     * @return construtor construtor da entidade
     * @throws NoSuchMethodException
     * @throws SecurityException
     */
    @SuppressWarnings("unchecked")
    protected Constructor<X> getExceptionConstructor(Class<?>... tipos) throws NoSuchMethodException, SecurityException {
        Class<X> theType = (Class<X>) ClassUtil.getTypeArguments(getClass(), 1);
        Constructor<X> constructor = theType.getConstructor(tipos);
        return constructor;
    }

    /**
     * Metódo responsável pela criação de uma nova exceção do tipo X
     *
     * @param message
     * @return X
     */
    @SuppressWarnings("unchecked")
    protected X getNewException(String message) {
        X exception = null;
        try {
            Constructor<X> constructor = getExceptionConstructor(String.class);
            exception = constructor.newInstance(message);
        } catch (Exception e) {
            getLogger().error(e);
        }
        return exception;
    }

    /**
     * Metódo responsável pela criação de uma nova exceção do tipo X
     *
     * @param cause
     * @return X
     */
    @SuppressWarnings("unchecked")
    protected X getNewException(Throwable cause) {
        X exception = null;
        try {
            Constructor<X> constructor = getExceptionConstructor(Throwable.class);
            exception = constructor.newInstance(cause);
        } catch (Exception e) {
            getLogger().error(e);
        }
        return exception;
    }

    /**
     * Metódo responsável pela criação de uma nova exceção do tipo X
     *
     * @param message
     * @param cause
     * @return X
     */
    @SuppressWarnings("unchecked")
    protected X getNewException(String message, Throwable cause) {
        X exception = null;
        try {
            Constructor<X> constructor = getExceptionConstructor(String.class, Throwable.class);
            exception = constructor.newInstance(message, cause);
        } catch (Exception e) {
            getLogger().error(e);
        }
        return exception;
    }

    /**
     * Metódo utilizado para lançar uma excessão do tipo X
     *
     * @param ex
     * @param operacao
     */
    protected void lancarException(Throwable ex, int operacao) throws ApplicationException {
    	SQLException sqle = extractSQLException(ex);
        if (sqle != null) {
            String detail = sqle.getMessage();
            if (getEntityClass().isAnnotationPresent(Constraints.class)) {
                Constraints constraints = getEntityClass().getAnnotation(Constraints.class);
                Constraint[] cvs = constraints.value();
                for (Constraint c : cvs) {
                    if (detail.contains(c.name())) {
                        String[] message = c.message();
                        String aux = message[0];
                        Set<String> parametros = new LinkedHashSet<String>();
                        for (int j = 1; j < message.length; j++) {
                            parametros.add(getMessageFactory().getMessage(message[j]));
                        }
                        String msg = getErrorMsg(operacao, aux, parametros.toArray());
                        throw getNewException(msg, ex);
                    }
                }
            }
            if (operacao == REMOVER && sqle.getSQLState().equals("23503")) {
                String msg = getErrorMsg(operacao, "item.erro.existem.registros");
                throw getNewException(msg, ex);
            }
            if (sqle.getMessage().contains("ERRO: ")) {
                String msg = sqle.getMessage().replace("ERRO: ", "");
                throw getNewException(msg, ex);
            }
            getLogger().error(detail, ex);
        }
        if (ex instanceof Exception) {
            throw getNewException(getErrorMsg(operacao), ex);
        }
    }

    /**
     * Método utilizado para extrair uma SQLException
     *
     * @param ex exception
     * @return SQLException sqlException
     */
    protected SQLException extractSQLException(Throwable ex) {
        SQLException sqlException = null;
        if (ex instanceof SQLException) {
            sqlException = (SQLException) ex;
        } else if (ex.getCause() != null) {
            sqlException = extractSQLException(ex.getCause());
        }
        return sqlException;
    }

    /**
     * Checa a ocorrencia de determinada contraint e lança um exception
     *
     * @param ex
     * @param contraint
     * @param msg
     * @throws X
     */
    protected void checarConstraintException(Throwable ex, String contraint, String msg) throws X {
    	SQLException sqle = extractSQLException(ex);
        if (sqle != null) {
            String detail = sqle.getMessage();
            if (detail.contains(contraint)) {
                throw getNewException((msg != null) ? msg : ex.getMessage(), ex);
            }
        }
    }

    /**
     * Remove todos os espaços em branco dos campos de uma entidade
     *
     * @param entidade
     */
    protected void removerEspacosVazios(E entidade) {
        if (entidade != null) {
            Field[] fields = entidade.getClass().getDeclaredFields();
            for (Field field : fields) {
                String name = field.getName();
                if (!name.equals("serialVersionUID")
                        && !isAnnotationInProperty(entidade, field, Transient.class)) {
                    field.setAccessible(true);
                    Class<?> type = field.getType();
                    boolean isString = String.class.isAssignableFrom(type);
                    if (isString) {
                        try {
                            String valorCampo = (String) field.get(entidade);
                            if (valorCampo != null) {
                                valorCampo = valorCampo.trim();
                                field.set(entidade, valorCampo);
                            }
                        } catch (Exception ex) {
                            getLogger().error(ex);
                        }
                    }
                }
            }
        }
    }

    /**
     * Transforma todos os campos String da entidade para maiúsculo
     *
     * @param entidade
     */
    protected void toUpperCase(Object entidade) {
        if (entidade != null) {
            Field[] fields = entidade.getClass().getDeclaredFields();
            for (Field field : fields) {
                String name = field.getName();
                if (!name.equals("serialVersionUID") && !name.toUpperCase().contains("SENHA")
                        && !isAnnotationInProperty(entidade, field, Transient.class)) {
                    field.setAccessible(true);
                    Class<?> type = field.getType();
                    try {
                        if (String.class.isAssignableFrom(type)) {
                            String valorCampo = (String) field.get(entidade);
                            if (valorCampo != null && !valorCampo.isEmpty()) {
                                valorCampo = valorCampo.toUpperCase();
                            } else {//Limpar campos vazios
                                valorCampo = null;
                            }
                            field.set(entidade, valorCampo);
                        } else if (Character.class.isAssignableFrom(type)) {
                            Character valorCampo = (Character) field.get(entidade);
                            if (valorCampo != null && Character.isLetterOrDigit(valorCampo)) {
                                valorCampo = Character.toUpperCase(valorCampo.charValue());
                            } else {//Limpar campos vazios
                                valorCampo = null;
                            }
                            field.set(entidade, valorCampo);
                        } else if (List.class.isAssignableFrom(type)) {
                            Type retorno = field.getGenericType();
                            if (retorno instanceof ParameterizedType) {
                                ParameterizedType pt = (ParameterizedType) retorno;
                                Type[] typeArguments = pt.getActualTypeArguments();
                                if (typeArguments.length > 0) {
                                    Class<?> typeArgClass = (Class<?>) typeArguments[0];
                                    if (typeArgClass.isAnnotationPresent(Entity.class)) {
                                        List<?> lista = (List<?>) field.get(entidade);
                                        if (lista != null && !lista.isEmpty()) {
                                            for (Object item : lista) {
                                                toUpperCase(item);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    } catch (Exception ex) {
                        getLogger().error(ex);
                    }
                }
            }
        }
    }

    /**
     * Método utilizado para formatar a entidade
     *
     * @param entidade
     */
    protected void formatarEntidade(E entidade) {
        toUpperCase(entidade);
        removerEspacosVazios(entidade);
    }

    /**
     * Verifica a existência de uma anotação num atributo de um objeto
     *
     * @param objeto
     * @param field
     * @param classAnnotation
     * @return boolean
     */
    protected boolean isAnnotationInProperty(Object objeto, Field field, Class<? extends Annotation> classAnnotation) {
        String name = field.getName();
        boolean retorno = field.isAnnotationPresent(classAnnotation);
        if (!retorno && objeto != null && name != null) {
            try {
                String nomeMetodo = "get" + name.substring(0, 1).toUpperCase();
                if (name.length() > 1) {
                    nomeMetodo += name.substring(1);
                }
                Class<?> classe = objeto.getClass();
                Method methodGet = classe.getMethod(nomeMetodo, new Class<?>[]{});
                retorno = methodGet.isAnnotationPresent(classAnnotation);
            } catch (Exception e) {
                getLogger().error(e);
            }
        }
        return retorno;
    }

    @SuppressWarnings("unchecked")
    private void lerListas(Object aux) throws SecurityException {
        Method[] metodos = aux.getClass().getDeclaredMethods();
        for (Method method : metodos) {
            Type retorno = method.getGenericReturnType();
            if (List.class.isAssignableFrom(method.getReturnType())
                    && retorno instanceof ParameterizedType) {
                ParameterizedType type = (ParameterizedType) retorno;
                Type[] typeArguments = type.getActualTypeArguments();
                if (typeArguments.length > 0) {
                    Class<?> typeArgClass = (Class<?>) typeArguments[0];
                    if (typeArgClass.isAnnotationPresent(Entity.class)) {
                        try {
                            List<?> lista = (List<?>) method.invoke(aux, new Object[]{});
                            for (Object item : lista) {
                                lerListas(item);
                            }
                        } catch (Exception ex) {
                            getLogger().error(ex);
                        }
                    }
                }
            }
        }
    }
}
